﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Entropy.Ext.Ast;
using Void = Entropy.Ext.Ast.Void;

namespace Entropy.Ext
{
	public class AsgBuilder : IAsgBuilder
	{
		private readonly IAccent accent;
		private readonly IComponentNaming naming;

		public AsgBuilder(IAccent accent, IComponentNaming naming)
		{
			this.accent = accent;
			this.naming = naming;
		}

		IEnumerable<Node> IAsgBuilder.Compile(Expression[] expressions)
		{
			var pronouncedNames = new Dictionary<Node, string>();
            var predefinedNames = new Dictionary<Node, string>();
            var root = new Block
			{
				Children = SemanticParser.ParseExpressions(expressions, accent, pronouncedNames, predefinedNames)
			};
			RemoveConstructorAssignmentInDelegates(root, pronouncedNames);
			RemoveReferenceAssignmentInDelegates(root);
            InstanciateConstructors(root, pronouncedNames, predefinedNames);
			ProperlyAssignEvents(root);
            InlineOnceUsedVariables(root);
            InverseConditionalBranchesIfFirstIsEmpty(root);
            InverseNegativeBinaryOperators(root);
			CollapseDoubleNotOperators(root);
			ReplaceGetMethodWithPropertyAccess(root);
            ReplacePropertyAccessOfAspPageWithGlobalReferences(root);
			return root.Children;
		}

	    private void ReplacePropertyAccessOfAspPageWithGlobalReferences(Block root)
	    {
	        root.FindAndReplace<PropertyGet>(s => (s.Object is Param) && ((Param)s.Object).Name == "this" , s => new Reference {Variable = new Variable {Name = s.PropName}});
	    }

	    private void InlineOnceUsedVariables(Block root)
	    {
	        var paramsUsage = (from n in root.Descendants()
	            where n is Param
	            group n by n
	            into g
	            select new
	            {
	                Node = g,
	                Count = g.Count()
	            }).ToArray();
	        var localsToInline = paramsUsage.Where(p => p.Count == 2).ToArray();
//            root.FindAndReplace<Binary>(
//                u =>
//                    u.Operator == BinaryOperator.Assign
//                    && u.Left is Param
//                u => new SpaceHolder(), );
        }

	    private static void RemoveReferenceAssignmentInDelegates(Block root)
		{
			root.FindAndReplace<Binary>(
                u =>
	                u.Operator == BinaryOperator.Assign
	                && u.Left is Param
	                && u.Right is Reference,
                u => new SpaceHolder());
		}

		private static void RemoveConstructorAssignmentInDelegates(Block root, Dictionary<Node, string> pronouncedNames)
		{
			root.FindAndReplace<Binary>(
		        u =>
		        u.Operator == BinaryOperator.Assign 
		        && u.Left is Param 
		        && u.Right is VariableDeclareOrRefer,
		        u =>
		            {
                        pronouncedNames.Add( ((VariableDeclareOrRefer)u.Right).Ctor, ((Param) u.Left).Name);
		                return u.Right;
		            });
			
		}

		private static void CollapseDoubleNotOperators(Block root)
		{
			root.FindAndReplace<Unary>(
			    u =>
			    u.Operator == UnaryOperator.Not && u.Operand is Unary &&
			    ((Unary) u.Operand).Operator == UnaryOperator.Not, u => ((Unary) u.Operand).Operand);
		}

		private static void ReplaceGetMethodWithPropertyAccess(Block root)
		{
			root.FindAndReplace<Call>(u => u.FunctionName.StartsWith("get_") && u.Arguments.Length == 0, u => new PropertyGet { Object = u.Object, PropName = u.FunctionName.Substring(4)});
		}


	    private void InverseConditionalBranchesIfFirstIsEmpty(Block root)
        {
            // if (test) else doSmth -> if (!test) doSmth else ;
            root.FindAndReplace<Conditional>(
                c => c.IfTrue is Default, c =>
                {
                    c.Test = new Unary {Operand = c.Test, Operator = UnaryOperator.Not};
                    var save = c.IfTrue;
                    c.IfTrue = c.IfFalse;
                    c.IfFalse = save;
                    return c;
                }
            );
        }


		private static void InverseNegativeBinaryOperators(Block root)
		{
			root.FindAndReplace<Unary>(u => u.Operator == UnaryOperator.Not && u.Operand is Binary, u =>
			{
				var b = (Binary) u.Operand;
			    if (InverseBinaryOp(b)) 
                   return b;

			    if (b.Operator.In(BinaryOperator.OrElse, BinaryOperator.AndAlso) && (b.Left.IsUnaryNot() || b.Right.IsUnaryNot()))
				{
					b.Operator = b.Operator == BinaryOperator.OrElse ? BinaryOperator.AndAlso : BinaryOperator.OrElse;
				    Func<Node, Node> applyNot = node => node.IsUnaryNot() 
                        ? ((Unary) node).Operand 
                        : new Unary {Operand = node, Operator = UnaryOperator.Not};
                    b.Left = applyNot(b.Left);
                    b.Right = applyNot(b.Right);
					return b;
				}
				return u;
			});
		}

	    private static bool InverseBinaryOp(Binary bin)
	    {
	        switch (bin.Operator)
	        {
	            case BinaryOperator.Equal:
                    bin.Operator = BinaryOperator.NotEqual;
	                return true;

	            case BinaryOperator.NotEqual:
                    bin.Operator = BinaryOperator.Equal;
	                return true;

	            case BinaryOperator.GraterThanOrEqual:
                    bin.Operator = BinaryOperator.LessThan;
	                return true;

	            case BinaryOperator.GreaterThan:
                    bin.Operator = BinaryOperator.LessThanOrEqual;
	                return true;

	            case BinaryOperator.LessThanOrEqual:
                    bin.Operator = BinaryOperator.GreaterThan;
	                return true;

	            case BinaryOperator.LessThan:
                    bin.Operator = BinaryOperator.GreaterThanOrEqual;
	                return true;
	        }
	        return false;
	    }

	    private void ProperlyAssignEvents(Block root)
		{
			// replace  grid.beforeedit = f to grid.on('beforeedit', f) 
			root.FindAndReplace<Binary>(b => b.Operator == BinaryOperator.Assign && b.Right is FunctionDefinition, assign => accent.ResolveEvent(assign));
		}

        private void InstanciateConstructors(Block root, Dictionary<Node, string> pronouncedNames, Dictionary<Node, string> predefinedNames)
        {
            var varByCtor = new Dictionary<Call, Binary>();
            root.FindAndReplace<VariableDeclareOrRefer>(n => true, n => UniteAndNameConstructors(n, varByCtor, pronouncedNames, predefinedNames));
        }

	    private Node UniteAndNameConstructors(VariableDeclareOrRefer unresolved, Dictionary<Call, Binary> varByCtor,
	        Dictionary<Node, string> pronouncedNames, Dictionary<Node, string> predefinedNames)
	    {
	        string name;
	        Binary instance;
	        if (varByCtor.TryGetValue(unresolved.Ctor, out instance))
	            return new Reference
	            {
	                Variable = (Variable) instance.Left
	            };

	        bool isPredefined = predefinedNames.TryGetValue(unresolved.Ctor, out name);
	        if (!isPredefined && !pronouncedNames.TryGetValue(unresolved.Ctor, out name))
	            name = naming.IssueVarName(unresolved.VarNameBasis);

	        var result = new Binary
	        {
	            Left = new Variable
	            {
	                Name = name
	            },
	            Right = unresolved.Ctor,
	            Operator = BinaryOperator.Assign
	        };

	        varByCtor.Add(unresolved.Ctor, result);
	        if (isPredefined)
	            return Void.Value;
	        return result;
	    }
	}

}